查看原文
其他

AOE工程实践-Tengine组件

杨科 极客人生THE GEEKS 2022-09-09
小编推荐:AOE.SDK开源以后,不少的热心的小伙伴提出希望增加对Tengine框架的支持,最近AOE已经完成Tengine组件的开发,并且在内部的一个工程里验证。下面我们来了解一下什么是Tengine,AOE的Tengine组件是如何做的。

Tengine是由 OPEN AI LAB 开发的一款轻量级模块化高性能神经网络推理引擎,专门针对Arm嵌入式设备优化,并且无需依赖第三方库,可跨平台使用支持Android,Liunx,并支持用GPU、DLA作为硬件加速计算资源异构加速。

在AOE开源工程里,我们最新提供了Android平台的Tengine组件,下面我们以SqueezeNet物体识别这个Sample为例,来讲一讲Tengine组件是如何设计的,以及它的用法。
 
1.

Android平台直接集成

Tengine的问题

为SqueezeNet接入Tengine,需要把相关的模型文件,Tengine的头文件和库,JNI调用,前处理和后处理相关业务逻辑等,都放在SqueezeNet Sample工程里。这样简单直接的集成方法,问题也很明显,和业务耦合比较多,不具有通用性,前处理后处理都和SqueezeTengine这个Sample有关,不能很方便地提供给其他业务组件使用。深入思考一下,如果我们把AI业务,作为一个一个单独的AI组件提供给业务的同学使用,会发生这样的情况:

每个组件都要依赖和包含Tengine的库,而且每个组件的开发同学,都要去熟悉Tengine的接口,写C的调用代码,写JNI,这真的是比较麻烦和头疼的事情。另外当你想要在底层从A推理框架换到B推理框架的时候,还需要去修改底层调用推理框架的部分。所以我们很自然地会想到要提取一个Tengine的组件出来,就像这样:   

通过对不同的推理框架的封装,对业务层提供统一的Java层接口来调用。这样调用变得简单了,而且如果要从Tengine组件切换到其他AOE提供的推理框架组件,只要修改依赖的库就可以了,不用再去修改底层AI推理框架的调用代码。

 
2.

AOE SDK里的Tengine组件

是如何实现的

在AOE开源SDK里,我们提供了Android版本的Tengine组件,下面我们从5个方面来讲一讲Tengine组件:

 
  • Tengine组件的设计

  • 对SqueezeNet Sample的改造

  • 如何在Native层调用Tengine组件

  • Tengine组件和AOE SDK的关系

  • 对Tengine组件的一些思考

Tengine组件的设计

Tengine组件的设计理念是组件里不包含具体的业务逻辑,只包含对Tengine接口的封装和调用。具体的业务逻辑,由业务方在外部实现。在接口定义和设计上,我们参考了TF Lite的源码和接口设计。目前提供的对外调用接口,主要有以下几个:
// 加载Tengine模型void loadTengineModel(...)// 初始化是否成功boolean isLoadModelSuccess()// 输入rgba数据void inputRgba(...)// 单输入进行推理,得到单输出数据void run(...)// 多输入进行推理,得到多输出的数据void runForMultipleInputsOutputs(...)// 得到推理结果Tensor getOutputTensor(...)// 关闭和清理内存void close()
AOE Tengine组件有以下几个特点:
  • 支持多输入多输出。

  • 使用Object作为输入和输出(支持了ByteBuffer和多维数组)。

  • 使用ByteBuffer来提升效率,支持Direct ByteBuffer。


下面我们来说说具体是如何做的。
如何支持多输入多输出
为了支持多输入和多输出,我们根据graph的输入和输出的node数量,和每个node对应的Tensor数量,计算出输入和输出的Tensor总数量。在Native层对应创建了一个我们自定义的Tensor对象列表,每个Tensor对象里保存了对Tengine里的tensor_t指针的引用。Native层的Tensor对象,通过tensor_jni提供给java层调用,Java层维护这个指向native层tensor的“指针”地址。这样在有多输入和多输出的时候,只要拿到这个列表里的对应的Tensor,就可以就行数据的操作了。在执行推理的的时候,把输入的数据,依次设置到输入的Tensor对象列表里的每个tensor_t里面,推理完成以后,从输出的Tensor对象列表里,获取到输入的tensor_t对象,就可以拿到模型推理后的数据了。
如何使用Object作为输入和输出
目前我们支持ByteBuffer和MultiDimensionalArray。在实际的操作过程中,如果是ByteBuffer,我们会判断是否是directbuffer,来进行不同的读写操作。如果是MultiDimensionalArray,我们会根据不同的数据类型(例如int, float等),维度信息等,来对数据进行读写拷贝等操作。
如何提升数据传输效率
我们更建议使用ByteBuffer作为输入和输出:ByteBuffer,字节缓存区处理子节的,比传统的数组的效率要高。DirectByteBuffer,使用的是堆外内存,省去了数据到内核的拷贝,因此效率比用ByteBuffer要高。使用了ByteBuffer以后,给我们带来的好处:
  • 接口里的字节操作更加便捷,例如里面的putInt,getInt,putFloat,getFloat,flip等一系列接口,可以很方便的对数据进行操作。

  • 和Native层做交互,可以使用DirectByteBuffer,提升效率。我们可以简单理解为java层和native层可以直接对一块“共享”内存进行操作,减少了中间的字节的拷贝过程。以图片为例,输入和输出流程如下图所示:
如果InputBuffer我们使用Direct Buffer,很明显可以减少从Native层到Java层的数据拷贝过程。
 

对SqueezeNet Sample的改造

集成AOE Tengine组件以后,让SqueezeNet依赖Tengine Module,SqueezeNet Sample里面只包含了模型文件,前处理和后处理相关的业务逻辑,前处理和后处理可以用java,也可以用c来实现,由具体的业务实现来决定。新的代码结构变得非常简洁。具体可以参考我们的sample工程。

如何在Native层调用Tengine组件

Tengine自身提供的API,是可以直接在Native层进行调用的。AOESDK,为了更快捷地适配不同的推理框架,我们在Natice层做了一层代理,可以通过引入Tengine组件和头文件的方式,在Native层进行Tengine组件的调用。

Tengine组件和AOE SDK的关系

对Tengine组件的接入,有两种方式
 
  • 直接接入

  
  • 通过AOE SDK接入 

AOE SDK提供独立进程,模型配置和动态升级,模型准确率和性能等数据分析,图像和加密组件等,我们更建议是通过AOE SDK来对我们的Tengine组件进行接入。 

3.
对Tengine组件的总结和思考

通过对Tengine组件的封装,现在业务集成Tengine更加快捷方便了。之前我们一个新的业务集成Tengine,可能需要一天的时间。使用AOE Tengine组件以后,可能只需要1-2小时的时间。
我们会通过不断的学习,加深对Tengine的理解,持续的对Tengine组件进行改造和优化。
AoE (AI on Edge,终端智能,边缘计算) 是一个终端侧AI集成运行时环境 (IRE),帮助开发者提升效率。欢迎大家来使用和提建议, https://github.com/didi/aoe

--------- PUHUI TECH ---------

本文作者
-
杨科
滴滴 | 资深软件开发工程师

热爱生活,热爱技术,热爱分享。在即时通讯领域有丰富的经验。我的人生格言是,每天进步一点点。

编辑 | 钱维
-
推荐阅读

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存